package de.gameduell.framework.util {
	import de.gameduell.framework.debug.Debug;

	import flash.events.EventDispatcher;
	import flash.events.Event;
	import flash.media.SoundChannel;
	import flash.media.SoundTransform;
	import flash.utils.Timer;
	import flash.events.TimerEvent;
	import flash.media.Sound;	

	/**
	 * Play effect and ambient sounds directly from the library.
	 * Handles soundChannels, their volumes and can even mute sounds.
	 * This singleton will be usually initialized by the base
	 * application.
	 * 
	 * @see de.gameduell.framework.application.Application
	 */
	public class Audio extends EventDispatcher {
		private static var instance:Audio = null;
		private var library:Library;
		private var soundChannels:Array;
		private var ambientChannels:Array;
		private var _soundVolume:Number;
		private var _ambientVolume:Number;
		private var _muteEffects:Boolean;
		private var _muteAmbient:Boolean;

		/**
		 * @return singleton instance of Audio. 
		 */
		public static function getInstance():Audio {
			if(instance == null) {
				instance = new Audio();
			}
			return instance;
		}

		/**
		 * Singleton class - do not call it directly.
		 */
		public function Audio() {
			library = Library.getInstance();
			soundChannels = new Array();
			ambientChannels = new Array();
			_muteEffects = false;
			_muteAmbient = false;
			_soundVolume = 1;
			_ambientVolume = 1;
		}

		/**
		 * Returns the current ambient sound volume.
		 * @return a value between 0 and 1.
		 */
		public function get ambientVolume():Number {
			return _ambientVolume;
		}

		/**
		 * Returns the current effect sound volume. 
		 * @return a value between 0 and 1.
		 */
		public function get soundVolume():Number {
			return _soundVolume;
		}

		/**
		 * Playing an effect sound which is stored in the library.
		 * 
		 * @param className			classId of a sound in the library
		 * @param startTime			optional startPosition time offset
		 * @param loops				optional loop count
		 * @param soundTransform	optional SoundTransform-object which should adjust the sound
		 * 
		 * @return	a SoundChannel which is holding the sound object
		 */
		public function playSound(className:String, startTime:Number = 0, loops:int = 0, soundTransform:SoundTransform = null):SoundChannel {			
			var soundChannel:SoundChannel = null;	
			if(!_muteEffects) {
				try {
					var sound:Sound;
					sound = library.createSound(className);
					soundChannel = sound.play(startTime, loops, soundTransform);
					soundChannel.soundTransform = new SoundTransform(_soundVolume);
					soundChannel.addEventListener(Event.SOUND_COMPLETE, soundComplete);
					soundChannels.push(soundChannel);
				}catch(e:Error) {
					soundChannels = new Array();
					ambientChannels = new Array();
				}
			}
			return soundChannel;
		}

		/**
		 * After a sound has finished playback, it's soundChannel will be removed.
		 * 
		 * @param event thrown by a completed sound.
		 */
		private function soundComplete(event:Event):void {
			soundChannels.splice(soundChannels.indexOf(event.target), 1);	
		}

		/**
		 * Stops every effect sound which is stored in the soundChannel array.
		 * If a computer does not have any active sound hardware, a warning will be traced.
		 */
		public function stopSounds():void {		
			while(soundChannels.length > 0) {
				try {
					SoundChannel(soundChannels.shift()).stop();
				}catch(e:Error) {
					Debug.trace("Audio::stopSounds: no sound channel available: " + e, Debug.WARNING);
				}
			}
		}

		/**
		 * Number of currently used soundChannels.
		 * @return soundChannel count
		 */
		public function get soundAmount():int {
			return soundChannels.length;
		}

		/**
		 * Override the currently used effect sound volume.
		 * If a computer does not have any active sound hardware, a warning will be traced.
		 * 
		 * @param volume	a new sound volume. This should be a number between 0 and 1.
		 */
		public function setSoundVolume(volume:Number):void {
			_soundVolume = volume;
			for(var i:* in soundChannels) {
				try {
					SoundChannel(soundChannels[i]).soundTransform = new SoundTransform(volume);
				}catch(e:Error) {
					Debug.trace("Audio::playSound: no sound channel available: " + e, Debug.WARNING);
				}
			}
		}

		/**
		 * Plays an ambient sound which is stored in the library.
		 * 
		 * @param className			classId of a sound in the library
		 * @param startTime			optional startPosition time offset
		 * @param loops				optional loop count - usually it will run "forever"
		 * @param soundTransform	optional SoundTransform-object which should adjust the sound
		 * @param fade				optional flag, makes the sound fade in
		 * 
		 * @return	a soundChannel which is holding the sound object
		 */
		public function playAmbient(className:String, startTime:Number = 0, soundTransform:SoundTransform = null,
									loops:int = 9999999, fade:Boolean = false, fadeInTime:int = 1000):SoundChannel {
			try {
				var soundChannel:SoundChannel = library.createSound(className).play(startTime, loops, soundTransform);
				soundChannel.addEventListener(Event.SOUND_COMPLETE, onAmbientSoundComplete);
				ambientChannels.push(soundChannel);
				if(!_muteAmbient) {
					if(fade) {
						setAmbientVolume(0);
						fadeIn(fadeInTime);
					} else {
						setAmbientVolume(_ambientVolume);
					}
				} else {
					changeAmbientVolume(0);
				}
				return soundChannel;
			}catch(e:Error) {
				soundChannels = new Array();
				ambientChannels = new Array();
			}
			return null;
		}

		/**
		 * Stops and removes all currently active ambient soundChannels.
		 * If a computer does not have any active sound hardware, a warning will be traced.
		 * @param fade		flag, makes the sound fade out, also if already silent!
		 */ 
		public function stopAmbients(fade:Boolean = false, fadeOutTime:int = 1000):void {
			if(fade && !_muteAmbient) {
				fadeOut(fadeOutTime);		
			} else {
				while(ambientChannels.length > 0) {
					try {
						SoundChannel(ambientChannels.shift()).stop();
					}
				catch(e:Error) {
						Debug.trace("Audio::stopAmbients: no sound channel available: " + e, Debug.WARNING);
					}	
				}
			}	
		}

		/**
		 * Overrides the current ambient sound volume value.
		 * 
		 * @param volume	a new sound volume. This should be a number between 0 and 1.
		 */
		public function setAmbientVolume(volume:Number):void {
			_ambientVolume = volume;
			if(!_muteAmbient) {
				changeAmbientVolume(volume);
			}
		}

		/**
		 * Adjusting the volume of all ambient sounds which are currently active.
		 * If a computer does not have any active sound hardware, a warning will be traced.
		 * 
		 * @param volume	a new sound volume. This should be a number between 0 and 1.
		 */
		private function changeAmbientVolume(volume:Number):void {
			for(var i:* in ambientChannels) {
				try {
					SoundChannel(ambientChannels[i]).soundTransform = new SoundTransform(volume);
				}catch(e:Error) {
					Debug.trace("Audio::playSound: no sound channel available: " + e, Debug.WARNING);
				}
			}	
		}

		/**
		 * Stops and removes both ambient and effect sounds at once.
		 */
		public function stopAll():void {
			stopSounds();
			stopAmbients();
		}

		/**
		 * Mutes and removes both ambient and effect sounds at once.
		 * After unmuting all sound have to be started again.
		 */
		public function toggleMute():void {
			if(!_muteAmbient || !_muteEffects) {
				_muteAmbient = true;
				_muteEffects = true;
				stopAll();
			}
			else if(_muteAmbient && _muteEffects) {
				_muteAmbient = false;
				_muteEffects = false;
			}
		}

		/**
		 * Stops and removes all effect sounds.
		 * Effect sounds can't be unmuted. This means you have to wait
		 * until they will be called again.
		 */
		public function toggleMuteSound():void {
			if(!_muteEffects) {
				_muteEffects = true;
				stopSounds();
			}
			else if(_muteEffects) {
				_muteEffects = false;
			}
		}

		/**
		 * Switches the ambient sound to mute state or vice versa.
		 * The sound is always running - even when it's muted. Otherwise
		 * the ambient sound will start from scratch again if you un-mute it.
		 * This can be really annoying ...
		 */
		public function toggleMuteAmbient():void {
			if(!_muteAmbient) {
				_muteAmbient = true;
				changeAmbientVolume(0);
			}
			else if(_muteAmbient) {
				_muteAmbient = false;
				setAmbientVolume(_ambientVolume);
			}
		}

		/**
		 * Mutes and removes both ambient and effect sounds at once.
		 */
		public function mute():void {
			_muteAmbient = true;
			_muteEffects = true;
			stopAll();
		}

		/**
		 * Redispatch an amibient sound complete event.
		 */
		private function onAmbientSoundComplete(event:Event):void {
			dispatchEvent(event);
		}

		private function fadeIn(fadeInTime:int = 1000):void {
			var faderSpeed:Number = fadeInTime / 10;
			var timer:Timer = new Timer(faderSpeed, 10); 
			timer.addEventListener(TimerEvent.TIMER, onFadeInTimer);
			timer.start();
		}

		private function onFadeInTimer(e:TimerEvent):void {
			var target:Timer = e.target as Timer;
			setAmbientVolume(target.currentCount / 10);
			if(target.currentCount == 10){
				target.removeEventListener(TimerEvent.TIMER, onFadeInTimer);
			}
		}

		private function fadeOut(fadeOutTime:int = 1000):void {
			var faderSpeed:Number = fadeOutTime / 10;
			var timer:Timer = new Timer(faderSpeed, 10); 
			timer.addEventListener(TimerEvent.TIMER, onFadeOutTimer);
			timer.start();
		}

		private function onFadeOutTimer(e:TimerEvent):void {
			var target:Timer = e.target as Timer;
			setAmbientVolume((10 - target.currentCount) / 10);
			if (target.currentCount == 10) target.removeEventListener(TimerEvent.TIMER, onFadeOutTimer);
		}

		public function get soundEnabled():Boolean {
			return !_muteEffects;
		}

		public function get musicEnabled():Boolean {
			return !_muteAmbient;
		}
	}
}
